// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Author: wsfuyibing <682805@qq.com>
// Date: 2024-07-17

package src

import (
	"context"
	"gitee.com/go-libs/log"
	"sync/atomic"
	"time"
)

type (
	// Locker
	// is an interface of locker.
	Locker interface {
		// Release
		// applied resource.
		Release()

		// Renew
		// prevent distributed lock from being deleted due to timeout.
		Renew(seconds ...int)
	}

	locker struct {
		connection *Connection
		ctx        context.Context
		key        string
		duration   time.Duration
		value      int32

		renewCanceller                         context.CancelFunc
		renewCalls, renewRunning, renewUpdates int32
	}
)

func (o *locker) Release() {
	// Stops renew goroutine.
	if o.renewCanceller != nil {
		o.renewCanceller()

		// Wait previous renew done.
		for {
			if atomic.LoadInt32(&o.renewRunning) == 0 {
				break
			}
			time.Sleep(time.Millisecond)
		}
	}

	// Delete
	// locked resource.
	if del := o.connection.Del(o.ctx, o.key); del != nil {
		if log.Config().DebugOn() {
			affect, _ := del.Uint64()
			log.Debugfc(o.ctx, `[redis][lock] succeed to delete distributed lock: key="%s", affect="%d"`, o.key, affect)
		}
	}

	// Close connection.
	if err := o.connection.Close(); err != nil {
		if log.Config().DebugOn() {
			log.Debugfc(o.ctx, `[redis][lock] failed to close connection`)
		}
	} else {
		if log.Config().DebugOn() {
			log.Debugfc(o.ctx, `[redis][lock] succeed to close connection`)
		}
	}
}

func (o *locker) Renew(seconds ...int) {
	if n := atomic.AddInt32(&o.renewCalls, 1); n > 1 {
		atomic.AddInt32(&o.renewCalls, -1)
		return
	}
	if o.ctx != nil {
		go o.renew(seconds...)
	}
}

func (o *locker) Update() {
	// Update total counter.
	count := atomic.AddInt32(&o.renewUpdates, 1)

	// Update running counter.
	atomic.AddInt32(&o.renewRunning, 1)
	defer atomic.AddInt32(&o.renewRunning, -1)

	// Send command.
	cmd := o.connection.Expire(o.ctx, o.key, o.duration)
	yes, err := cmd.Result()
	if err != nil {
		log.Errorfc(o.ctx, `[redis][lock] failed to renew: key="%s", error="%v"`, o.key, err)
	} else if log.Config().DebugOn() {
		log.Debugfc(o.ctx, `[redis][lock] renew completed: count="%d", succeed="%v"`, count, yes)
	}
}

func (o *locker) renew(seconds ...int) {
	var (
		ctx       context.Context
		frequency = DefaultLockRenew
		tick      *time.Ticker
	)

	// Generate
	// frequency seconds.
	if len(seconds) > 0 && seconds[0] > 0 {
		frequency = seconds[0]
	}
	if log.Config().DebugOn() {
		log.Debugfc(o.ctx, `[redis][lock] renew started: frequency="%ds"`, frequency)
	}

	// Create
	// runtime fields.
	ctx, o.renewCanceller = context.WithCancel(o.ctx)
	tick = time.NewTicker(time.Duration(frequency) * time.Second)

	// Loop
	// channel messages.
	for {
		select {
		case <-tick.C:
			go o.Update()

		case <-ctx.Done():
			{
				tick.Stop()
				if log.Config().DebugOn() {
					log.Debugfc(o.ctx, `[redis][lock] renew stopped`)
				}
			}
			return
		}
	}
}
